Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.

...powered by www.netzwerkartist.de...

 << zurück
Visual C# 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2005

Visual C# 2005
1.320 S., mit 2 CDs, 59,90 Euro
Galileo Computing
ISBN 3-89842-586-X
gp Kapitel 11 Multithreading und asynchrone Methodenaufrufe
  gp 11.1 Prozesse und Threads
    gp 11.1.1 Threadzustände und Prioritäten
    gp 11.1.2 Einsatz von mehreren Threads
  gp 11.2 Die Entwicklung einer Multithread-Anwendung
    gp 11.2.1 Die Klasse »Thread«
    gp 11.2.2 Threadpools nutzen
  gp 11.3 Die Synchronisation von Threads
    gp 11.3.1 Unsynchronisierte Threads
    gp 11.3.2 Der »Monitor« zur Synchronisation
    gp 11.3.3 Das Synchronisationsobjekt »Mutex«
    gp 11.3.4 Das Attribut »MethodImpl«
  gp 11.4 Asynchrone Methodenaufrufe
    gp 11.4.1 Asynchroner Methodenaufruf
    gp 11.4.2 Asynchroner Aufruf mit Rückgabewerten
    gp 11.4.3 Eine Klasse mit asynchronen Methodenaufrufen


Galileo Computing

11.2 Die Entwicklung einer Multithread-Anwendundowntop

Im folgenden Beispiel wird auf einfachste Weise neben dem Hauptthread, der beim Starten einer Anwendung automatisch erzeugt wird, ein zweiter Thread per Programmcode ins Leben gerufen. Anhand dieses kleinen Programms wollen wir uns mit den wichtigsten Grundlagen einer multithreading-fähigen Anwendung vertraut machen.


// ------------------------------------------------------------
// Beispiel: ...\Kapitel 11\EinfacherThread
// ------------------------------------------------------------ 
class Program {
  static void Main(string[] args) {
    ThreadStart del;
    del = new ThreadStart(MyProcedure);
    Thread myFirstThread = new Thread(del);
    // den zweiten Thread starten
    myFirstThread.Start();
    for(int i = 0; i <= 100; i++) {
      for(int k = 1; k <= 20; k++)
        Console.Write(".");
      Console.WriteLine("Primär-Thread " + i);
    }
    Console.ReadLine();
  }
  // diese Methode wird in einem eigenen Thread ausgeführt
  public static void MyProcedure() {
    for(int i = 0; i <= 100; i++) {
      for(int k = 1; k <= 20; k++)
        Console.Write("x");
      Console.WriteLine("Sekundär-Thread " + i);
    }
  }
}

Alle Klassen, die mit der Entwicklung multithreading-fähiger Anwendungen unter .NET in Zusammenhang stehen, sind im Namespace System.Threading zu finden, der am Anfang des Programms mit using bekannt gegeben werden sollte. Die wichtigste Klasse innerhalb dieses Namespace dürfte die Klasse Thread sein, mit der ein neuer Thread erzeugt wird. Werfen wir einen Blick auf den eingesetzten Konstruktor dieser Klasse:


public Thread(ThreadStart start); 

Bei dem Parameter vom Typ ThreadStart handelt es sich um einen Delegaten, der die Methode angibt, deren Anweisungen in einem neuen Thread ausgeführt werden sollen. Die Definition dieses Delegaten lautet wie folgt:


public sealed delegate void ThreadStart();

Die Instanz eines Delegaten kapselt den Zeiger auf die Speicheradresse einer Methode. Die Typen der Parameterliste des Delegaten müssen den Typen der Parameterliste der Methode entsprechen, auf die der Delegat verweist. Demzufolge kann man dem Konstruktor der Klasse Thread über dem Delegaten nur die Adresse einer parameterlosen Methode zuweisen – in unserem Beispiel ist es die Methode MyProcedure.


ThreadStart del = new ThreadStart(MyProcedure);
Thread myFirstThread = new Thread(del);

Im ersten Schritt wird die Variable del vom Typ des Delegaten ThreadStart deklariert. Dem Delegat wird die Adresse der benutzerdefinierten Methode übergeben. Danach kann die Thread-Klasse unter Übergabe der Referenz des Delegaten instanziiert werden. Mit


Thread myFirstThread = new Thread(new ThreadStart(MyProcedure));

können Sie den Code auch einzeilig formulieren, da die Referenz auf den Delegaten nicht mehr benötigt wird.

Die Instanziierung der Thread-Klasse ist noch nicht ausreichend, um den zweiten Thread der Anwendung zu aktivieren. Entscheidend ist vielmehr die Methode Start des Thread-Objekts:


myFirstThread.Start();

Mit dem Start der Anwendung wird bereits der erste Thread, der Primärthread, automatisch erstellt – gekennzeichnet durch den Aufruf der Main-Methode. Der zweite Thread wird erst mit Start zum Leben erweckt. Nun wird auch MyProcedure ausgeführt, allerdings unabhängig vom Primärthread in einem eigenen Thread.

Beide Threads arbeiten zwei ineinander geschachtelte Schleifen ab. Die Schleifen sind so konstruiert, dass eine Zeitscheibeneinheit nicht ausreicht, um jeweils vollständig die Schleifen zu durchlaufen, denn dann könnten wir den Effekt des Multithreadings an der Konsole nicht erkennen. In der innersten Schleife wird eine Ausgabe in die Konsole geschrieben, die 20 Punkte enthält und daran anschließend beschreibt, welcher Thread für die Ausgabe verantwortlich ist. Dem wird zusätzlich noch der aktuelle Stand des Zählers der äußeren Schleife angehängt.

Schauen wir uns nun die Ausgabe an, die abhängig von der Hardware-Ausstattung, der Systemkonfiguration und anderen laufenden Anwendungen durchaus anders aussehen kann. Die Interpretation der Ausgabe hilft, die Arbeitsweise der Threads im Zusammenhang mit der Zeitscheibe und der quasiparallelen Ausführung zu verstehen.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.2   Die Ausgabe der Anwendung »EinfacherThread«

Es fällt auf, dass nicht alle Schleifendurchläufe eines Threads vom Start- bis zum Endwert des äußeren Schleifenzählers erfolgen, sondern sich die beiden Threads bei der Ausgabe abwechseln. Zuerst erzeugt der Primärthread 20 Punkte, gibt sich anschließend namentlich bekannt und schreibt sechs weitere Punkte in die nächste Ausgabezeile. Dann ist seine Zeitscheibe abgelaufen. Er räumt die CPU und begibt sich in die Warteschlange. Nun kommt der Sekundärthread zum Zuge, der immerhin acht vollständige Ausgabezeilen schreiben kann. Für eine vollständige neunte reicht die Zeit jedoch nicht mehr aus, denn nach dem neunzehnten »x« muss der Sekundärthread die CPU für den Primärthread räumen. Dieses Wechselspiel setzt sich so lange fort, bis beide Threads ihre Aufgabe vollständig abgearbeitet haben.

Der Delegat »ParameterizedThreadStart«

Den Delegaten ThreadStart, mit dem die in einem separaten Thread laufende Methode beschrieben wird, haben wir behandelt. ThreadStart hat jedoch ein Manko, denn diese Methode muss parameterlos sein. Manchmal ist es aber notwendig, der Threadmethode Daten zu übergeben. In der Version 2.0 des .NET Frameworks wird jetzt diese Lücke geschlossen. Dazu wird uns eine Alternative mit dem Delegaten ParameterizedThreadStart geboten:


public sealed delegate void ParameterizedThreadStart(object obj);

Die Instanz eines solchen Delegaten kapselt den Zeiger auf eine Methode, welche die Referenz auf ein beliebiges Objekt erwartet. Hier werden uns alle Türen geöffnet, denn wir können, falls mehrere Daten an die Threadmethode übergeben werden sollen, auch ein Array oder eine Auflistung angeben.

Die Erzeugung des Threads erfolgt in bekannter Weise. Der einzige Unterschied ist im Konstruktor der Klasse Thread zu finden, dem wir nicht eine Instanz von ThreadStart, sondern von ParameterizedThreadStart übergeben:


Thread thread = new Thread(new ParameterizedThreadStart(ThreadMethod));

Um die gewünschten Daten an die Threadmethode zu leiten, greifen wir auf eine Überladung der Start-Methode zu, der wir das entsprechende Argument mitteilen:


thread.Start(IrgendEinObjekt);

Das übergebene Objekt enthält die Daten, die von der vom Thread ausgeführten Methode verwendet werden sollen.


Galileo Computing

11.2.1 Die Klasse »Thread«  downtop

Ein Thread wird erzeugt, wenn die Klasse Thread unter Übergabe eines Delegaten instanziiert wird. Dies stellt nicht die einzige Möglichkeit dar, sich die Referenz auf einen Thread zu besorgen.

Zugriff eines Threads auf sich selbst

Wenn es beispielsweise notwendig ist, auf dem Hauptthread Operationen auszuführen, steht Ihnen diese Referenz explizit nicht zur Verfügung, da der Thread implizit beim Start der Anwendung erzeugt wird. Abhilfe schafft die statische Eigenschaft CurrentThread, die eine Referenz auf den aktuellen Thread liefert:


public static Thread CurrentThread {get;}

Nehmen wir an, dass ein Thread seine eigene Priorität mit der Eigenschaft Priority erhöhen soll, müssten Sie


Thread.CurrentThread.Priority = Prioritätswert;

codieren, damit der Thread auf sich selbst zugreifen kann. Auf die Eigenschaft Priority kommen wir später zu sprechen.

Einen Thread für eine bestimmte Zeitdauer anhalten

Im Beispiel oben wurde eine Schleife eingebaut, um eine kleine Zeitverzögerung zu erreichen. Ohne Schleife könnte es – abhängig von der Taktfrequenz des Prozessors – sein, dass die gesamte Schleife des ersten Threads bereits vollständig abgearbeitet ist, bevor der zweite Thread zum ersten Mal in seine eigene Schleife eintritt. Die Thread-Klasse bietet für solche Fälle mit der Methode Sleep eine bessere Alternative, einen Thread für eine bestimmte Zeitdauer anzuhalten und damit die Ausführung zu verzögern.


public static void Sleep(int);
public static void Sleep(TimeSpan);

Beachten Sie, dass diese Methoden statisch definiert sind und nicht auf eine bestimmte Threadinstanz aufgerufen werden können, sondern nur aus dem Code des aktuell laufenden Threads heraus, der sich damit selbst aus dem Verkehr zieht. Im Gegensatz zu den im ersten Beispiel verwendeten Schleifen zur Simulation einer länger andauernden Threadoperation ist Sleep unabhängig von der Taktfrequenz des Computers.

Nur der Thread selbst kann bestimmen, ob er sich selbst zur Ruhe legt oder nicht, ein anderer Thread hat keinen Einfluss darauf. Sie rufen Sleep auf, indem Sie entweder die Anzahl der Millisekunden angeben, die sich der Thread zurückziehen soll, oder Sie übergeben eine Referenz auf ein TimeSpan-Objekt.

Den Effekt, den wir mit Thread.Sleep erzielen können, wollen wir uns an einem Beispiel ansehen.


// ------------------------------------------------------------
// Beispiel: ...\Kapitel 11\SleepingThread
// ------------------------------------------------------------ 
class Program {
  static void Main(string[] args) {
    ThreadStart del;
    del = new ThreadStart(MyProcedure);
    Thread firstThread = new Thread(del);
    firstThread.Start();
    // die Schleife läuft im Primärthread
    for(int i = 0; i <= 100; i++) {
      Thread.Sleep(20);
      Console.WriteLine("Primär-Thread " + i);
    }
    Console.ReadLine();
  }
  // diese Prozedur wird in einem zweiten Thread ausgeführt
  static void MyProcedure() {       
    for(int i = 0; i <= 100; i++) {
      Thread.Sleep(5);
      Console.WriteLine("Sekundär-Thread " + i);
    }
  }
}

Auch in diesem Beispiel wird eine Methode MyProcedure in einem zweiten Thread ausgeführt. Der Primärthread wird 20 ms stillgelegt, der Sekundärthread für jeweils nur 5 ms pro Schleifendurchlauf. Damit erhält der Sekundärthread circa viermal so viel Prozessorzeit wie der Primärthread, was sich auch an der Konsole zeigt (siehe Abbildung 11.3).

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.3   Konsolenausgabe des Beispiels »SleepingThread«

Bisher wissen Sie, dass Sleep eine Zeitspanne in Form eines Integers oder einer Referenz auf ein TimeSpan-Objekt übergeben wird, um die Ruhezeit des Threads zu beschreiben. Es gibt noch zwei weitere Möglichkeiten, Sleep einzusetzen, was ein bisher noch nicht erörtertes Verhalten aufzwingt:

1.  die Übergabe des Wertes 0
2. die Übergabe von Timeout.Infinite
       

Wird Sleep die Zahl 0 übergeben, wird der Thread dazu veranlasst, auf den verbleibenden Rest seiner Ausführungszeit zu verzichten und die CPU für den nächsten anstehenden Thread frei zu machen. Er reiht sich danach sofort wieder in die Warteschlange ein.

Wartende Threads

Ein Thread, der sich auf eine unbestimmte Zeit zur Ruhe begibt, ruft die Sleep-Methode mit dem Wert Timeout.Infinite auf, also:


Thread.Sleep(Timeout.Infinite)

Die Klasse Timeout ist wohl so mager ausgestattet wie kaum eine andere des .NET Frameworks. Sie enthält nur die Konstante Infinite, die den Wert –1 repräsentiert. Ein auf diese Weise eingefrorener Thread kommt nicht mehr automatisch zum Zuge, wenn es darum geht, vom Scheduler ein Stück Zeitscheibe zu erhaschen, denn er ist nicht bereit, sondern er wartet. Dieser Zustand kann nur durch einen anderen Thread aufgehoben werden. Dazu ruft der aktive Thread die Methode Interrupt auf den wartenden Thread auf:


// Starten des primäre Threads
static void Main(string[] args) {
  ThreadStart del = new ThreadStart(MyMethod);
  Thread newThread = new Thread(del);
  newThread.Start();
  ...
  newThread.Interrupt();
}
// diese Methode wird in einem zweiten Thread ausgeführt
static void MyMethod() {
  ...
  // Thread auf unbestimmte Zeit einfrieren
  Thread.Sleep(Timeout.Infinite);
}

Die Sleep-Methode ist also leistungsfähiger, als es im ersten Augenblick den Anschein hat. Fassen wir die möglichen Argumente noch einmal kurz in tabellarischer Form zusammen:


Tabelle 11.1   Die Argumente der »Sleep«-Methode

Übergabewert Beschreibung
Zahl > 0 Der aktuelle Thread verlässt den Prozessor und versetzt sich für die in Millisekunden angegebene Zeit in den Zustand wartend. Nach Ablauf der Zeit wird er automatisch in den Zustand bereit versetzt und wartet auf die CPU-Zuteilung durch den Scheduler.
0 Der aktuelle Thread gibt die CPU sofort frei und reiht sich in die Warteschlange zur CPU ein. Sein Zustand ist bereit.
Timeout.Infinite Der aktuelle Thread verlässt den Prozessor und versetzt sich auf unbestimmte Zeit in den Zustand wartend. Er kann nur durch einen anderen Thread unter Aufruf der Methode Interrupt wieder als bereit in die Warteschlange eingereiht werden.

Einem anderen Thread die Zeitscheibe entziehen

Mit der Methode Sleep kann sich ein Thread selbst einfrieren, auf einen anderen Thread kann diese Methode nicht aufgerufen werden. Soll ein Thread einem anderen die Zeitscheibe entziehen, muss er die Methode Suspend auf den entsprechenden Thread aufrufen:


thread.Suspend();

Ein Thread kann diese Methode auch auf sich selbst aufrufen. Das kommt dann der Übergabe der Konstanten Timeout.Infinite an Sleep gleich.

Der Aufruf von Suspend auf einen Thread, der seine Ausführung beendet hat oder noch nicht gestartet worden ist, führt zu der Ausnahme ThreadStateException. Besteht die Gefahr, auf einen solchen Thread zu treffen, muss der Zustand des betreffenden Threads zuvor ermittelt werden. Dazu greift man auf ein Member der Enumeration ThreadState zurück.


Tabelle 11.2   Die Member der Enumeration »ThreadState«

Member Beschreibung
Aborted Der Thread ist mit Abort beendet worden.
AbortRequested Ein anderer Thread hat das Terminieren angefordert.
Background Der Thread ist ein Hintergrundthread.
Running Der Thread wird ausgeführt oder ist zumindest bereit.
Stopped Der Thread ist beendet.
StopRequested Es gibt eine Anforderung für das Beenden des Threads.
Suspended Der Thread wurde unterbrochen.
SuspendRequested Ein anderer Thread hat das Unterbrechen des Threads angefordert.
Unstarted Die Start-Methode wurde auf den Thread noch nicht aufgerufen.
WaitSleepJoin Auf den Thread wurde Wait, Sleep oder Join aufgerufen. Der Thread ist blockiert.

Die Member der Enumeration ThreadState werden bitweise kombiniert. Wollen Sie sicherstellen, dass beim Aufruf von Suspend keine Ausnahme ausgelöst wird, müssen Sie daher, wie nachfolgend gezeigt, den Zustand des Threads abfragen:


if(((thread.ThreadState & ThreadState.Stopped) != 0) &&
  ((thread.ThreadState & ThreadState.Unstarted) != 0))
  thread.Suspend();

Ein Thread, dem mit Suspend die Zeitscheibe entzogen wurde, muss wieder angestoßen werden, bevor er seine ihm zugedachte Aufgabe weiter bearbeiten kann. Dazu wird die Methode Resume auf die Referenz des wartenden Threads aufgerufen. Auf einen Thread darf mehrfach Suspend ausgeführt werden, ohne dass es zu einer Fehlermeldung kommt. Allerdings reicht ein einziges Resume, um den Thread wieder in die Warteschlange auf die Zeitscheibe einzureihen.

Im folgenden Beispielprogramm wird der Einsatz der Methoden Suspend und Resume demonstriert. In der darauf folgenden Abbildung 11.4. ist die Ausgabe des Programms zu sehen.


// ------------------------------------------------------------
// Beispiel: ...\Kapitel 11\SuspendThread
// ------------------------------------------------------------
class Program {
  static void Main(string[] args) {
    int i = 0;
    Thread thread = new Thread(new ThreadStart(Test));
    thread.Start();
    while(true) {
      i++;
      Console.Write("x");
      if(i == 200) {
        Console.WriteLine("\nUnterbrechen des Sekundärthreads");
        // Sekundärthread die Zeitscheibe entziehen
        thread.Suspend();
      }
      if (i == 1000) {
        Console.WriteLine("Primärthread beendet – Starten Sekundärthread");
        break;
      }
    }
    // Sekundärthread in die Warteschlange stellen
    thread.Resume();
    Console.ReadLine();
  }
  // Methode wird im Sekundärthread ausgeführt
  public static void Test() {
    int i = 0;
    while(true) {
      i++;
      Console.Write(".");
      if(i == 1000) break;
    }
  }
}

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.4   Die Konsolenausgabe des Beispiels »SuspendThread«

Der Aufruf von Suspend oder Resume auf einen Thread, der nicht gestartet oder bereits beendet worden ist, führt zur Ausnahme ThreadStateException. Dieselbe Ausnahme wird auch ausgelöst, falls Resume auf einen Thread trifft, der nicht suspendiert worden ist.

Sicheres Beenden eines Threads

Einem Thread auf eine bestimmte oder unbestimmte Zeit die Zeitscheibe zu entziehen, ist eine Sache. Eine andere ist das Terminieren eines Threads mit der Methode Thread.Abort. Der Aufruf bewirkt in der Laufzeitschicht die Auslösung der Ausnahme ThreadAbortException. Damit ist es möglich, die Methode ordnungsgemäß zu beenden, beispielsweise um dabei offene Ressourcen zu schließen.

Dazu zunächst ein Beispiel. Diesmal wird die Routine, die in einem zweiten Thread ausgeführt wird, in einer eigenen Klasse definiert. Damit ändert sich grundsätzlich nichts, da dem Delegaten nun die Adresse der Instanzmethode in der Klasse mitgeteilt wird.


// ------------------------------------------------------------
// Beispiel: ...\Kapitel 11\AbortThread
// ------------------------------------------------------------
class Program {
  static void Main(string[] args) {
    ClassA obj = new ClassA();
    ThreadStart firstThread;
    firstThread = new ThreadStart(obj.ThreadExecution);
    Thread TheThread = new Thread(firstThread);
    Console.WriteLine("Thread wird jetzt gestartet");
    // sekundären Thread starten
    TheThread.Start();
    Console.WriteLine("Thread ist gestartet");
    // der sekundäre Thread wird durch den Primärthread mit 
    // der Methode Abort zerstört
    Thread.Sleep(200);
    TheThread.Abort();
    Thread.Sleep(100);
    if (TheThread.IsAlive)
      Console.WriteLine("Der Sek.-Thread lebt noch");
    else
      Console.WriteLine("Der Sek.-Thread ist aufgegeben");
    Thread.Sleep(5000);
  }
}
class ClassA {
  public void ThreadExecution() {
    try {
      Console.WriteLine("Sek.-Thread gestartet.");
      // die Schleife zwingt dem Thread eine länger 
      // andauernde Ausführung auf
      for(int i = 0; i <= 100; i++) {
        Console.WriteLine("Sek.-Thread-Zähler = {0}", i);
        Thread.Sleep(50);
      }
    }
    // die Angabe von 'ThreadAbortException e' kann im catch- 
    // Zweig unterbleiben, weil wir die Referenz 'e' nicht
    // benutzen
    catch {
      Console.WriteLine("Sek.-Thread/im Catch-Block");
    }
    finally {
      Console.WriteLine("Sek.-Thread/in Finally");
    }
    Console.WriteLine("Sek.-Thread/nach Finally");
    for (int i = 0; i <= 20; i++) {
      Console.Write(".");
      Thread.Sleep(50);
    }
  }
}

Sehen wir uns zuerst den Programmcode an. Nach dem Instanziieren der Thread-Klasse wird der zweite Thread gestartet. Da wir die Abort-Methode testen wollen, müssen wir dafür sorgen, dass Abort nicht auf einen Thread trifft, der nicht mehr ausgeführt wird. Deshalb ist in ThreadExecution der Klasse ClassA eine Schleife eingebaut, die eine längere Zeit für einen vollständigen Durchlauf benötigt. Die Zeit muss so groß angesetzt werden, dass Abort auf die sich noch in Arbeit befindliche Schleife trifft.

Vor dem Aufruf von Abort wird der Primärthread zunächst mit Sleep gebremst, damit der Sekundärthread etwas Zeit zu arbeiten hat. Nach dem Aufruf von Abort bekommt das System mit einem zweiten Sleep-Aufruf noch Zeit, den Sekundärthread endgültig zu beenden. Durch Auswertung der Eigenschaft IsAlive auf dem Sekundärthread wird festgestellt, ob dieser noch aktiv ist oder nicht. Würden wir dem Hauptthread keine Ruhepause gönnen, könnte eine falsche Aussage die Folge sein, da die if-Bedingungsprüfung vor der Aufgabe des Sekundärthreads durchgeführt wird, weil sich Abort und if innerhalb derselben Zeitscheibe befinden und der freigegebene Thread noch keine Möglichkeit erhalten hat, die Ausnahme auszulösen. Die zweite Schleife in der Methode ThreadExecution der Klasse ClassA soll ebenfalls eine länger andauernde Operation simulieren.

An der Konsole erfolgt die folgende Ausgabe:


Thread wird jetzt gestartet
Thread ist gestartet
Sek.-Thread gestartet
Sek.-Thread-Zähler = 0
Sek.-Thread-Zähler = 1
Sek.-Thread-Zähler = 2
Sek.-Thread-Zähler = 3
Sek.-Thread/in Catch-Block
Sek.-Thread/in Finally
Der Sek.-Thread ist aufgegeben

Festzustellen ist ein anscheinender Widerspruch zu der Aussage in Kapitel 9, dass die hinter finally stehenden Anweisungen ausgeführt werden: Der Aufruf von Abort löst die Exception ThreadAbortException aus, aber die zweite Schleife im Sekundärthread wird nicht mehr durchlaufen. Genau in diesem Punkt liegt das Besondere dieser Ausnahme, denn sie wird ausgelöst und auch gefangen, aber die Anweisungen hinter dem Ende der Ausnahmebehandlung kommen nicht mehr zur Ausführung, da der Thread in diesem Moment bereits terminiert ist. Allerdings unterstützt die Laufzeitschicht abschließende Anweisungen in finally.

Gegen das außerplanmäßige Beenden kann sich der betroffene Thread allerdings auch zur Wehr setzen. Dazu muss im catch-Block des Exceptionhandlers die statische Methode ResetAbort aufgerufen werden:


...
catch (ThreadAbortException e) {
  Thread.ResetAbort();
  Console.WriteLine("Sek.-Thread/im Catch-Block");
  ...  
}

Bauen Sie diese Anweisung in den Programmcode des Beispiels ein, wird auch die zweite Schleife in ThreadExecution ausgeführt, und die bedingte Prüfung mit if führt zu dem Ergebnis, dass der Thread noch lebt – das allerdings auch nur, weil die zweite Schleife ebenfalls wieder eine längere Zeit in Anspruch nimmt oder der Thread nicht schon auf normalen Wege aufgegeben worden ist, bevor die Prüfung erfolgt.

Abhängige Threads – die Methode »Join«

Nun wäre die folgende Ausgangssituation vorstellbar: Der Primärthread beendet den Sekundärthread mit Abort und muss dabei sicherstellen, dass die Anweisungen im Sekundärthread zuerst vollständig abgearbeitet sind, bevor die nächste Anweisung im Primärthread ausgeführt wird. Solche Situationen können auftreten, wenn der Code des Primärthreads auf das ordnungsgemäße Beenden angewiesen ist. Das heißt aber auch, dass der Aufruf synchron erfolgen muss, also auf die quasi gleichzeitige Ausführung, die ansonsten die Threads auszeichnet, bewusst verzichtet wird.

Wir wollen, um uns der Problematik bewusst zu werden, zunächst eine kleine Änderung in Main vornehmen. Die Implementierung der Klasse ClassA bleibt wie im Beispiel AbortThread erhalten (also ohne den Aufruf von ResetAbort, falls Sie damit experimentiert haben sollten).


static void Main(string[] args) {
  ClassA obj = new ClassA();
  ThreadStart firstThread;
  firstThread = new ThreadStart(obj.ThreadExecution);
  Thread TheThread = new Thread(firstThread);
  Console.WriteLine("Thread wird jetzt gestartet");
  // sekundären Thread starten
  TheThread.Start();
  Console.WriteLine("Thread ist gestartet");
  Console.WriteLine("vor Abort.............");
  // der sekundäre Thread wird durch den Primärthread mit 
  // der Methode Abort zerstört
  Thread.Sleep(200);
  TheThread.Abort();
  // die folgende Anweisung simuliert Code, der vom Beenden
  // des Sekundärthreads abhängig ist
  Console.WriteLine("nach Abort.............");
  Thread.Sleep(100);
  if (TheThread.IsAlive)
    Console.WriteLine("Der Sek.-Thread lebt noch");
  else
    Console.WriteLine("Der Sek.-Thread ist aufgegeben");
  Thread.Sleep(5000);
}

Main enthält eine Anweisung, die nach dem Aufruf der Abort-Methode die Konsolenausgabe


nach Abort................

erzwingt. Damit sollen Anweisungen simuliert werden, die auf das ordnungsgemäße Terminieren des sekundären Threads angewiesen sind. Sehen wir uns zunächst die Konsolenausgabe des Programmcodes in Abbildung 11.5 an.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.5   Abhängige Threads – unerwünschter Programmfluss

Deutlich ist zu erkennen, dass der sekundäre Thread nach Abort immer noch aktiv ist – die catch- und finally-Blöcke werden nach der abhängigen Anweisung ausgeführt.

Jetzt hilft eine andere Methode der Klasse Thread weiter: Join, die den aktuellen, also aufrufenden Thread so lange blockiert, bis der Sekundärthread vollständig terminiert ist. Sinnvollerweise wird Join direkt hinter Abort aufgerufen. Der Programmablauf kehrt erst dann zum Aufrufer zurück, wenn die Threadausführung ordnungsgemäß beendet ist.


// ------------------------------------------------------------
// Beispiel: ...\Kapitel 11\AbhängigerThread
// ------------------------------------------------------------ 
class Program {
  static void Main(string[] args) {
    ...
    // der sekundäre Thread wird durch den Primärthread mit 
    // der Methode Abort zerstört
    Thread.Sleep(200);
    TheThread.Abort();
    TheThread.Join();
    // die folgende Anweisung simuliert Code, der vom 
    // Beenden des Sekundärthreads abhängig ist
    Console.WriteLine("nach Abort.............");
    ...
  }
}
...

Wenn Sie dieses Programm starten, gibt die Konsole das folgende Ergebnis aus:

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.6   Ausgabe nach dem sicheren Beenden des Threads

Vergleichen wir diese Ausgabe mit der, die wir ohne Join hatten (Abbildung 11.5), können wir eindeutig erkennen, dass der Thread, dessen Terminierung angestoßen wurde, zuerst vollständig abgearbeitet wird, bevor der Aufrufer seinen eigenen Programmfluss fortsetzt.

Thread-Prioritäten festlegen

Jeder Thread hat eine Priorität. Mit der Eigenschaft Priority lässt sich die Priorität eines Threads erhöhen, verringern oder einfach nur auswerten. Die Priorität spielt eine entscheidende Rolle bei der Vergabe der Zeitscheibe: Ein Thread hat Vorrang vor einem anderen Thread mit niedrigerer Priorität – vorausgesetzt natürlich, dass sich beide durch den Zustand bereit beschreiben lassen.

Priority ist vom Typ der Enumeration ThreadPriority, die fünf Member definiert:

gp  ThreadPriority.Highest
gp  ThreadPriority.AboveNormal
gp  ThreadPriority.Normal
gp  ThreadPriority.BelowNormal
gp  ThreadPriority.Lowest

Die Prioritäten können von der höchsten Stufe (Threadpriority.Highest) bis zur niedrigsten (ThreadPriority.Lowest) eingestellt werden. Die automatisch einem Thread zugewiesene Priorität lautet Normal.

Der Thread mit der höchsten Priorität erhält die Zeitscheibe und läuft so lange, bis

gp  er mit Suspend gestoppt wird, sich selbst mit Sleep einfriert oder ganz einfach stirbt, weil seine Operationen beendet sind oder Abort auf ihn aufgerufen wird,
gp  ein Thread höherer Priorität lauffähig ist und Anspruch auf die CPU erhebt.

Am häufigsten ist der Fall anzutreffen, dass sich mehrere Threads gleicher Priorität in die Warteschlange zur CPU eingeordnet haben. Alle erhalten gleiche Zeitanteile nach einem Verfahren, das als Round-Robin-Verteilungsverfahren bezeichnet wird. Dabei wird karussellartig die Zeitscheibe auf die bereiten Threads verteilt.

Im folgenden Beispielprogramm wollen wir die Auswirkungen der Prioritätsfestlegung in einer Anwendung studieren.


// -------------------------------------------------------------
// Beispiel: ...\Kapitel 11\ThreadPriorität
// -------------------------------------------------------------
class Program {
  // Starten des primären Threads
  static void Main(string[] args) {
    ClassA obj = new ClassA();
    Thread thread1, thread2;
    thread1 = new Thread(new ThreadStart(obj.Execution1));
    thread2 = new Thread(new ThreadStart(obj.Execution2));
    // die Priorität von thread1 hoch setzen
    thread1.Priority = ThreadPriority.AboveNormal;
    // thread 1 starten
    thread1.Start();
    // thread 2 starten
    thread2.Start();
    Console.ReadLine();
  }
}
class ClassA {
  public void Execution1() {
    for (int i = 0; i <= 500; i++) {
      Console.Write(".");
    }
  }
  public void Execution2() {
    for (int number = 0; number <= 10; number++)
      Console.WriteLine("It's me,Thread2");
  }
}

Die im Kontext unserer Problemstellung wichtigen Codezeilen sind:


thread1.Priority = ThreadPriority.AboveNormal;
thread1.Start();
thread2.Start();

Um den Unterschied deutlich zu machen, empfiehlt es sich, beim ersten Versuch die Anweisung zur Erhöhung der Priorität des ersten Threads auszukommentieren. Starten Sie mit dieser Vorgabe die Laufzeit, werden Sie eine Konsolenausgabe wie in Abbildung 11.7 gezeigt erhalten. thread1 wird gestartet, schreibt ein paar Punkte in die Ausgabe und übergibt danach dem Prozessor den thread2, der sich durch eine eigene Zeichenfolge bemerkbar macht. Die Zeitscheibe dauert lang genug, um die Anweisungen von thread2 vollständig zu bearbeiten. Danach übernimmt wieder thread1 die CPU und beendet seine Ausführung.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.7   Konsolenausgabe ohne Hochsetzen der Priorität

Erhöhen wir nun die Priorität des ersten Threads um eine Stufe und starten ein zweites Mal die Laufzeit.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.8   Konsolenausgabe nach dem Hochsetzen der Priorität

Da thread1 nun die höchste Priorität aller sich in der Warteschlange befindlichen Threads hat (besser gesagt, ist zumindest die Priorität im Vergleich zu thread2 höher), werden seine Anweisungen zuerst vollständig ausgeführt, bevor thread2 mit seiner geringeren Priorität an die Reihe kommt.

Einem Thread eine gewisse Sonderstellung durch die Erhöhung der Priorität einzuräumen, mag vielleicht manchmal ganz verlockend klingen. Bedenken Sie jedoch, dass dieser Thread bei einer lang andauernden Operation eine bremsende Wirkung auf die anderen Threads hat. Man spricht auch von einem Aushungern des Systems. Gehen Sie daher sorgfältig mit dem Erhöhen von Prioritäten um, und achten Sie darauf, dass keine unnötigen Operationen von einem solchen Thread ausgeführt werden, sondern nur solche, die für den weiteren Ablauf der Anwendung unbedingt notwendig sind.

Vorder- und Hintergrundthreads

Threads werden in zwei Kategorien unterteilt: in Vorder- und in Hintergrundthreads. Ein Prozess wird ausgeführt, solange noch mindestens ein Vordergrundthread existiert. Mit dem Beenden des letzten Vordergrundthreads wird der Prozess der Anwendung selbst dann beendet, wenn Hintergrundthreads noch aktiv sind und die ihnen auferlegte Aufgabe noch nicht vollständig ausgeführt haben.

Die Eigenschaft IsBackground beschreibt, ob ein Thread als Vorder- oder Hintergrundthread eingestuft ist. Grundsätzlich sind alle Threads, die aus der Klasse Thread erzeugt werden, zunächst Vordergrundthreads. Mit IsBackground lässt sich ein Thread aber auch zu einem Hintergrundthread degradieren.

Im folgenden Beispielprogramm ist der Effekt des Unterschieds zwischen einem Vorder- und Hintergrundthread deutlich zu erkennen. In Main wird ein neuer Thread erzeugt und als Hintergrundthread festgelegt. Main läuft selbst in einem Vordergrundthread terminiert, bevor der Hintergrundthread seine Aufgabe vollständig ausgeführt hat – die Ausgabe des Hintergrundthreads an der Konsole ist unvollständig. Kommentieren Sie die Anweisung aus, in welcher der zweite Thread zum Hintergrundthread wird, wird der Prozess erst in dem Moment beendet, wenn beide Threads die ihnen zugestandene Aufgabe abgeschlossen haben.


// ------------------------------------------------------------
// Beispiel: ...\Kapitel 11\Hintergrundthread
// ------------------------------------------------------------
class Program {
  static void Main(string[] args) {
    Thread thread = new Thread(new ThreadStart(MyMethod));
    // neuen Thread zum Hintergrundthread machen
    thread.IsBackground = true;
    thread.Start();
    int i = 0;
    while(true){
      i++;
      Console.Write("x");
      Thread.Sleep(1000);
      if(i == 5) break;
    }
    // Vordergrundthread terminiert vor dem Hintergrundthread
  }
  // die folgende Methode wird in einem Hintergrundthread ausgeführt
  public static void MyMethod() {
    int i = 0;
    while(true) {
      i++;
      Console.Write(".");
      Thread.Sleep(2000);
      if(i == 10) break;
    }
  }
}


Galileo Computing

11.2.2 Threadpools nutzetoptop

Die Arbeit mit mehreren Threads lässt sich durch Threadpools wesentlich vereinfachen, denn die Laufzeitumgebung erzeugt eine bestimmte Anzahl von Threads, wenn sie gestartet wird. Sie können diese Threads nutzen und brauchen nicht eigens neue zu erzeugen, wenn Sie welche benötigen. Nach der Beendigung einer Threadmethode wird der frei gewordene Thread in den Pool zurück geführt und steht anderen Aufgaben zur Verfügung.

Angesprochen wird der Threadpool mit der gleichnamigen Klasse Threadpool. Mit deren statischer Methode QueueUserWorkItem wird der Threadpool aktiviert. Dabei wird der Methode ein Delegat vom Typ WaitCallback übergeben, der die Methode beschreibt, die mit dem Thread ausgeführt werden soll.

Grau ist die Theorie, daher sehen wir uns zuerst ein komplettes Beispiel an.


// --------------------------------------------------------------
// Beispiel: ...\ Kapitel 11\ThreadpoolDemo
// --------------------------------------------------------------
class Program {
  static void Main(string[] args) {
    // den Threadpool erforschen
    int maxThreads;
    int asyncThreads;
    ThreadPool.GetMaxThreads(out maxThreads, out asyncThreads);
    Console.WriteLine("Max. Anzahl Threads: {0}", maxThreads);
    Console.WriteLine("Max. Anzahl E/A-Threads: {0}", asyncThreads);
    Console.WriteLine(new string('-', 40));
    // Benachrichtigungsereignis, Zustand 'nicht signalisieren'
    AutoResetEvent ready = new AutoResetEvent(false);
    // Anfordern eines Threads aus dem Pool
    ThreadPool.QueueUserWorkItem(new WaitCallback(Calculate), ready);
    Console.WriteLine("Der Hauptthread wartet ...");
    // Hauptthread in den Wartezustand setzen
    ready.WaitOne();
    Console.WriteLine("Sekundärthread ist fertig.");
    Console.ReadLine();
  }
  public static void Calculate(object obj) {
    Console.WriteLine("Im Sekundärthread");
    Thread.Sleep(5000);
    // Ereigniszustand auf 'signalisieren' festlegen
    ((AutoResetEvent)obj).Set();
  }
}

Die Methode Calculate soll in einem Thread aus dem Threadpool ausgeführt werden. Bevor diese Operation eingeleitet wird, wollen wir aber noch feststellen, wie viele Threads uns der Pool zur Verfügung stellt, und rufen die statische Methode GetMaxThreads auf. Über den ersten Parameter werden uns die Threads geliefert, der zweite Parameter gibt darüber hinaus Auskunft über die maximale Anzahl der möglichen E/A-Anforderungen. Sie werden feststellen, dass sich 25 Threads im Pool befinden, und zwar pro Prozessor.

Das Beispiel ist so entwickelt, dass nicht nur ein Thread aus dem Pool zur Ausführung der Methode Calculate herangezogen wird. Darüber hinaus wird auch ein Synchronisationsszenario in Gang gesetzt, das bewirkt, dass während der Ausführung von Calculate der aufrufende Code in Wartestellung versetzt wird und auf ein Signal von Calculate wartet, bevor er seine Arbeit wieder aufnimmt. Mehr zur Synchronisierung erfahren Sie im folgenden Abschnitt.

Dem Aufruf der statischen Methode QueueUserWorkItem wird ein Delegat, der die im Thread auszuführende Methode beschreibt, übergeben. Darüber hinaus kann QueueUserWorkItem ein zweites Argument übergeben werden, um der Threadmethode Daten bereitzustellen. Hier wird dem zweiten Parameter ein Objekt vom Typ AutoResetEvent übergeben. Dieses Objekt versetzt zwei Threads in die Lage, über Signale miteinander zu kommunizieren. Erzeugt wird das Objekt im Code mit:


AutoResetEvent ready = new AutoResetEvent(false);

Der Übergabeparameter false besagt, dass der anfängliche Zustand des Objekts auf »nicht signalisiert« festgelegt wird. Mit


ready.WaitOne();

wird der aktuelle Thread so lange blockiert, bis er ein Signal erhält. Dieses stammt aus der Threadmethode und wird durch Aufruf der Set-Methode des AutoResetEvent-Objekts ausgelöst:


((AutoResetEvent)obj).Set();

Hier profitieren wir davon, der Threadmethode im zweiten Parameter die Referenz auf das AutoResetEvent übergeben zu haben.

 << zurück
  
  Zum Katalog
Zum Katalog: Visual C# 2005
Visual C# 2005
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Fortgeschrittene Programmierung mit Visual C# 2005






 Fortgeschrittene
 Programmierung
 mit Visual C# 2005


Zum Katalog: Einstieg in Visual C# 2005






 Einstieg in
 Visual C# 2005


Zum Katalog: Einstieg in Visual Basic 2005






 Einstieg in
 Visual Basic 2005


Zum Katalog: Visual Basic 2005






 Visual Basic 2005


Zum Katalog: Java ist auch eine Insel






 Java ist auch eine
 Insel


Zum Katalog: Konzepte und Lösungen für Microsoft-Netzwerke






 Konzepte und
 Lösungen für
 Microsoft-Netzwerke


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo








Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de